﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleCmdProc
{
    public class CommandLine
    {
        class Option
        {
            public Option(string name, string description, bool mandatory, bool allowMultiple)
            {
                this.name = name;
                this.description = description;
                this.mandatory = mandatory;
                this.allowMultiple = allowMultiple;
                this.defaultValue = null;
            }
            public Option(string name, string description, bool mandatory, bool allowMultiple, string defaultValue)
            {
                this.name = name;
                this.description = description;
                this.mandatory = mandatory;
                this.allowMultiple = allowMultiple;
                this.defaultValue = defaultValue;
            }
            public string name { get; set; }
            public string description { get; set; }
            public bool mandatory { get; set; }
            public bool allowMultiple { get; set; }
            public string defaultValue { get; set; }
        }

        class Flag
        {
            public Flag(string name, string description, bool mandatory)
            {
                this.name = name;
                this.description = description;
                this.mandatory = mandatory;
            }
            public string name { get; set; }
            public string description { get; set; }
            public bool mandatory { get; set; }
        }

        private List<Option> _definedOptions = new List<Option>();
        private List<Flag> _definedFlags = new List<Flag>();

        private List<Tuple<string, string>> _options = new List<Tuple<string, string>>();
        private List<string> _flags = new List<string>();
        private bool _supportHelpFlag = false;
        private List<string> _passedArguments;  // to support adding one by one
        private string _delim = "/";
        private string _errorStr;

        public CommandLine(bool supportHelpFlag)    // print usage when '/h' supplied
        {
            _supportHelpFlag = supportHelpFlag;
            _passedArguments = new List<string>();
        }

        public CommandLine()
            : this(false)
        {
        }

        /// <summary>
        /// Define an option (ex: /file="image.jpg")
        /// </summary>
        /// <param name="name"></param>
        /// <param name="description"></param>
        /// <param name="mandatory"></param>
        /// <param name="allowMultiple"></param>
        public void DefineOption(string name, string description, bool mandatory, bool allowMultiple)
        {
            // check if already defined
            foreach (Option opt in _definedOptions)
            {
                if (opt.name == name)
                    throw new Exception("This option is already defined.");
            }
            if (!mandatory)
            {
                throw new Exception("Non-mandatory options must specify defult value.");
            }
            _definedOptions.Add(new Option(name, description, mandatory, allowMultiple));
        }

        /// <summary>
        /// Define an option (ex: /file="image.jpg")
        /// </summary>
        /// <param name="name"></param>
        /// <param name="description"></param>
        /// <param name="mandatory"></param>
        /// <param name="allowMultiple"></param>
        /// <param name="defaultValue"</param>
        public void DefineOption(string name, string description, bool mandatory, bool allowMultiple, string defaultValue)
        {
            // check if already defined
            foreach (Option opt in _definedOptions)
            {
                if (opt.name == name)
                    throw new Exception("This option is already defined.");
            }
            _definedOptions.Add(new Option(name, description, mandatory, allowMultiple, defaultValue));
        }

        /// <summary>
        /// Define a flat (ex.: /d)
        /// </summary>
        /// <param name="name"></param>
        /// <param name="description"></param>
        /// <param name="mandatory"></param>
        public void DefineFlag(string name, string description, bool mandatory)
        {
            // check if already defined
            foreach (Flag flag in _definedFlags)
            {
                if (flag.name == name)
                    throw new Exception("This flag is already defined.");
            }
            _definedFlags.Add(new Flag(name, description, mandatory));
        }

        /// <summary>
        /// Look through all passed options and return values that correspond to this option
        /// </summary>
        /// <param name="option"></param>
        /// <returns></returns>
        public List<string> GetOptionValue(string option)
        {
            if (!IsOptionDefined(option))
                throw new Exception("Option not defined: " + option);

            List<string> values = new List<string>();

            foreach (Tuple<string, string> opt in _options)
            {
                if (opt.Item1 == option)
                    values.Add(opt.Item2);
            }
            return values;
        }

        /// <summary>
        /// Look through all passed flags, and it specified return 'true'
        /// </summary>
        /// <param name="flag"></param>
        /// <returns></returns>
        public bool GetFlagValue(string flag)
        {
            if (!IsFlagDefined(flag))
                throw new Exception("Flag not defined: " + flag);

            foreach (string str in _flags)
            {
                if (str == flag)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Return 'true' if an option is passed in
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public bool IsOptionSupplied(string value)
        {
            if (GetOptionValue(value).Count > 0)
                return true;
            else
                return false;
        }

        /// <summary>
        /// Return 'true' if a flag is passed in
        /// </summary>
        /// <param name="flag"></param>
        /// <returns></returns>
        public bool IsFlagSupplied(string flag)
        {
            if (GetFlagValue(flag))
                return true;
            else
                return false;
        }

        /// <summary>
        /// Return 'true' if an option or a flag is defined
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private bool IsOptionDefined(string value)
        {
            foreach (Option o in _definedOptions)
            {
                if (o.name == value)
                    return true;
            }
            return false;
        }

        /// <summary>
        /// Return 'true' if a flag is defined
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private bool IsFlagDefined(string value)
        {
            foreach (Flag f in _definedFlags)
            {
                if (f.name == value)
                    return true;
            }
            return false;
        }

        private string Usage
        {
            get
            {
                StringBuilder sb = new StringBuilder();
                sb.AppendLine("Usage:");
                if (_definedOptions.Count > 0)
                {
                    sb.AppendLine("    Options:");
                    foreach (Option o in _definedOptions)
                    {
                        if (o.mandatory)
                            sb.AppendLine(String.Format("        /{0}=<val>: {1}", o.name, o.description));
                        else
                            sb.AppendLine(String.Format("        [/{0}]=<val>: {1}", o.name, o.description));
                    }
                }
                if (_definedFlags.Count > 0)
                {
                    sb.AppendLine("    Flags:");
                    foreach (Flag f in _definedFlags)
                    {
                        if (f.mandatory)
                            sb.AppendLine(String.Format("        /{0}: {1}", f.name, f.description));
                        else
                            sb.AppendLine(String.Format("        [/{0}]: {1}", f.name, f.description));
                    }
                }
                return sb.ToString();
            }
        }

        /// <summary>
        /// Returns Defined option, or null if not defined
        /// </summary>
        /// <param name="opt"></param>
        /// <returns></returns>
        private Option GetDefinedOption(string opt)
        {
            foreach (Option o in _definedOptions)
            {
                if (o.name == opt)
                    return o;
            }
            return null;
        }

        /// <summary>
        /// Returns Defined flag, or null if not defined
        /// </summary>
        /// <param name="flag"></param>
        /// <returns></returns>
        private Flag GetDefinedFlag(string flag)
        {
            foreach (Flag f in _definedFlags)
            {
                if (f.name == flag)
                    return f;
            }
            return null;
        }

        /// <summary>
        /// Calling app can pass arguments one by one
        /// </summary>
        /// <param name="arg"></param>
        public void AddArgument(string arg)
        {
            _passedArguments.Add(arg);
        }

        /// <summary>
        /// Load arguments previously added
        /// </summary>
        /// <returns></returns>
        public bool LoadArguments()
        {
            return LoadArguments(_passedArguments.ToArray());
        }

        /// <summary>
        /// The main method to load the arguments passed into the application
        /// </summary>
        /// <param name="args"></param>
        /// <returns></returns>
        public bool LoadArguments(string[] args)
        {
            _errorStr = null;

            if (args.Length == 0)
            {
                _errorStr = "No arguments supplied.";
                Console.WriteLine(_errorStr);
                Console.WriteLine(Usage);
                return false;
            }

            if (_supportHelpFlag)
            {
                if (args[0] == (_delim + "h"))
                {
                    Console.WriteLine(Usage);
                    return false;
                }
            }

            StringBuilder errors = new StringBuilder();
            _options.Clear();
            _flags.Clear();

            foreach (string arg in args)
            {
                // each arg needs to start with '/'
                if (!arg.StartsWith(_delim))
                {
                    errors.AppendLine(String.Format("Invalid option or flag {0}. They must start with '/'.", arg));
                    continue;
                }

                string[] ap = arg.Split(new string[] { _delim, "=" }, StringSplitOptions.RemoveEmptyEntries);
                if (!(ap.Length == 1 || ap.Length == 2))
                    errors.AppendLine(String.Format("Invalid string for option or flag {0}.", arg));

                if (ap.Length == 2) // option
                {
                    string opt = ap[0];
                    string val = ap[1];
                    // check if this option is valid
                    Option defOpt = GetDefinedOption(opt);
                    bool error = false;
                    if (defOpt == null)
                    {
                        errors.AppendLine(String.Format("Unknown option supplied: {0}.", opt));
                        error = true;
                    }
                    else
                    {
                        // check if it is a duplicate
                        if (!defOpt.allowMultiple)
                        {
                            foreach (var o in _options)
                            {
                                if (o.Item1 == opt)   // already supplied!
                                {
                                    errors.AppendLine(String.Format("Option {0} supplied multiple times.", opt));
                                    error = true;
                                    break;
                                }
                            }
                        }
                    }

                    if (!error)// must be OK, add it
                        _options.Add(new Tuple<string, string>(opt, val));


                }
                else if (ap.Length == 1) // flag
                {
                    string flag = ap[0];
                    // check if this flag is valid
                    Flag defFlag = GetDefinedFlag(flag);
                    bool error = false;
                    if (defFlag == null)
                    {
                        errors.AppendLine(String.Format("Unknown flag supplied: {0}.", flag));
                        error = true;
                    }
                    else
                    {
                        // check if it is a duplicate
                        foreach (var f in _flags)
                        {
                            if (f == flag)
                            {
                                errors.AppendLine(String.Format("Flag {0} supplied multiple times.", flag));
                                error = true;
                                break;
                            }
                        }
                    }
                    if (!error) // must be OK, add it
                        _flags.Add(flag);
                }

            }

            // add defaults when options are not mandatory
            foreach (Option op in _definedOptions)
            {
                // if not supplied and is not mandatory, use the default value
                if (!IsOptionSupplied(op.name) && !op.mandatory)
                {
                    _options.Add(new Tuple<string, string>(op.name, op.defaultValue));
                }
            }

            // check if all mandatory options and flags are supplied
            foreach (Option op in _definedOptions)
            {
                if (op.mandatory)
                {
                    if (!IsOptionSupplied(op.name))
                        errors.AppendLine(String.Format("Mandatory option {0} not supplied.", op.name));
                }
            }
            foreach (Flag fl in _definedFlags)
            {
                if (fl.mandatory)
                {
                    if (!IsFlagSupplied(fl.name))
                        errors.AppendLine(String.Format("Mandatory option {0} not supplied.", fl.name));
                }
            }

            if (errors.Length > 0)
            {
                _errorStr = errors.ToString();
                Console.WriteLine(_errorStr);
                Console.WriteLine(Usage);
                return false;
            }

            return true;

        }

        /// <summary>
        /// Default is "/". Use this method to change to "-" for example (ex: -f="test.jpg")
        /// </summary>
        /// <param name="argChar"></param>
        public void SetArgChar(string argChar)
        {
            _delim = argChar;
        }

        /// <summary>
        /// Returns error string if any, otherwise null.
        /// </summary>
        public string Errors { get { return _errorStr; } }

    }
}
